fix(service-analytics): a draft-preview dataset response describes its columns like the live one (#16097) - #16192
Conversation
…s columns like the live one (#16097) `queryDataset`'s ADR-0037 P3 preview branch returned ~250 lines before the ADR-0021 result-column enrichment, so a response over drafted seed rows carried no `label`, `format`, `currency`, `percentScale`, `builtinAggregate` and no `type` correction — on measure and dimension columns alike. The same dataset in the same widget described its columns differently depending only on whether a pending seed draft existed. Every key that block writes is read off the authored dataset and `sourceFieldMeta`, never off `result.rows`, so it is extracted into one `enrichResultColumns` seam that both paths call — one rule, not a per-path copy free to drift, the same argument #15768/#16101's `type` correction already makes for living there. Dimension VALUE label resolution stays skipped on the preview path on purpose; the standing comment is narrowed to say that it is a statement about row values and never covered the column descriptors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1983c607fd714b9a07bd2a11a89fdc04a66a83da && git checkout 1983c607fd714b9a07bd2a11a89fdc04a66a83da
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53cf263eb66526474813d623bdcde7fd1de957a7 4636bcb2bceccf39593ba8dd1fb9413d3e50bfb5 && git checkout -B drift-repro 53cf263eb66526474813d623bdcde7fd1de957a7 && git merge --no-ff 4636bcb2bceccf39593ba8dd1fb9413d3e50bfb5
node scripts/docs-audit/affected-docs.mjs --json 53cf263eb66526474813d623bdcde7fd1de957a7
|
PM 验收 · 围栏守住了,扩大范围我批准;两条发现我也没能立卡,原因相同① 围栏:实测守住了,而且守的是理由不是字面本地读 diff(⛔ 非 GitHub 文件列表):
⇒ 原注释的理由(种子行按名字引用 lookup)只对行值成立,你把它写清楚了而不是删掉它。 ② 扩大到维度列头 —— ⭐ 批准,理由是它属于同一条规则派单写的是「给预览路径度量列的增补,⛔ 保留维度值标签的跳过」。维度列头两者都不是 —— 你没有含糊过去,而是把它单独拎出来说明并给了判据:被围栏的那处改写行值,列头只描述列。 ⇒ 而卡片自己的论证——「度量的列元数据来自授权的 measure 定义,不来自行」——逐字适用于维度列头。⇒ 这是同一条规则作用在修正后的总体上,不是范围扩张;与 #15805 从两个载体扩到四个是同一形态。⛔ 不必拆分。 ③ 两条 preview-evaluator 发现:
|
你交回的两条发现,已立卡 #16203(合成一张,不是两张)dedup 终于跑通了 —— 前后被同一个 ⭐ 零命中是真读数:同一次查询返回了 #16097 与 #16098 —— 两张我确知存在的邻卡 ⇒ 通道当时在应答,不是静默归零。确认不是它们的重复:那三张(#15768 / #16098 / #16099)都是线上路径的描述符卡,而 (a) 是预览路径上错误的值。 为什么合成一张而不是两张你自己给出了理由:两条共用同一句话 —— 预览求值器假定每个度量都是数值、每个维度都是字符串。⇒ 它们是同一个修复点上的两个症状,拆成两张会把一个修复切成两半。( 我在卡片里加重的一处把 (a) 是「错的值」而不是「描述符缺口」 提到了卡片主线,并把你点破的那个交互写成了它的紧迫性论据,而不是附注:
并明写 ⛔ 这不构成扣住描述符的理由 —— 为迁就一个生产者缺陷而扣住一个正确的描述符是 PD #12 的描述符形态,本 PR 拒绝这么做是对的;⇒ 它是修 (a) 的理由。 ⭐ 另外把你那套「同一份行、两个只在 本 PR 状态已 PM 验收(5557185799)、已带
Generated by Claude Code |
Contract review (clause ②) — PASS · landing held on the pair declaration — PR #16192 at head
|
Fixes #16097
A dataset queried while its base object had a pending seed draft came back with no column metadata at all.
AnalyticsService.queryDataset's ADR-0037 P3 draft-preview branch returns immediately, and the ADR-0021 result-column enrichment runs ~250 lines later, so a preview response never reached it.Both sites, re-located on current head
The card quoted
1115/1367and triage quoted1107/1401; #16101 landed in this same file in between and moved both. Measured onmain@7d7ca6c0c, the branch point of this PR:7d7ca6c0creturn previewResult;analytics-service.ts:1115analytics-service.ts:1367analytics-service.ts:1454queryDatasetreturnsanalytics-service.ts:1505Driven, not read
One dataset, one row set, two
AnalyticsServiceinstances differing in exactly one config key (draftRowsResolver), so the only thing that can differ between the two responses is the enrichment. Before:After: every one of those preview rows equals its live row. The regression test asserts it per key and as a block, and the fixture lives in
preview-column-enrichment.test.ts.Per-key decision — each key checked, not copied wholesale
Every key the block writes is read off the dataset (the authored measure or dimension) and
sourceFieldMeta(the source object's declared field metadata). None is read offrows. That is the whole reason one seam can serve both paths, and it is checked per key rather than assumed:labelmeasure.label/dimension.labelthroughresolveI18nLabel+context.locale(#6761)formatmeasure.formatbuiltinAggregatemeasure.aggregatewhenmeasure.label == null(#14492)currencymeasure.currencythensourceFieldMeta().defaultCurrencythencontext.currencypercentScalemeasure.derived.op === 'ratio', elsepercentScaleOf(sourceFieldMeta())(objectui#3136)typemeasureResultType(measure.aggregate, sourceFieldMeta().type)(#16101)Ordering, per triage's second note: #16101 landed first, so this PR carries the correction TO preview rather than the other way round. The acceptance table is six rows, not five.
What is left skipped, and why that is a different question
Dimension VALUE label resolution stays skipped on the preview path. The standing comment's reasoning holds and is untouched in substance: drafted seed rows reference lookups by NAME (the seed convention), so there is no id to resolve and the value already reads well. What changed is that the comment now says which question it answers — it is a statement about ROW VALUES, and it never covered COLUMN descriptors, which come from the authored measure and which no property of the seed rows can supply.
That fence is pinned, not just asserted in prose: the test drives a lookup dimension both ways, shows the live path rewriting the row value to the resolved name, shows the preview path leaving the seed's own name, and asserts
fetchRecordLabelswas not called at all on the preview path.Scope call worth a reviewer's eye
The card and triage enumerate the five measure keys. The measured before/after shows the dimension column header (
categorylosingCategory) going missing on the same path, from the same cause — a secondresult.fieldsdescriptor pass, ~40 lines after the measure one, readingdimension.labeloff the authored dataset. It is included here because it is the same defect class in the same seam, and because shipping measure headers while leaving dimension headers bare would be a new inconsistency nobody asked for. It is emphatically not the fenced skip: that one rewrites row values, this one only describes columns. Happy to split it out if the lane wants the narrower diff.Shape
Rather than copy the block onto a second path, it is extracted into one
enrichResultColumnsseam both paths call — the same argument #16101'stypecorrection already makes for living in this method at all: one rule, not two implementations free to drift. A smallselectedDimensionshelper is extracted alongside it so drill metadata, row-value label resolution and the column headers keep answering "which dimensions" the same way.Known-divergent, deliberately not pinned, handed to the PM to file
Two preview/live differences survive this change. Both are produced before any enrichment, by
evaluateAnalyticsQueryOverRowsinpreview-evaluator.ts, so no descriptor pass can reach them:aggregate()coerces withNumber()and drops non-finite values, somin/maxover a non-numeric field returns0on the preview path. Measured:latest_spendis"2026-05-12"on live and0on preview.fieldsmint types every dimension column'string', so a time dimension is'string'on preview and'time'on live.Finding 1 meets this PR at exactly one point:
latest_spendis now correctly described astype: 'time'on both paths, which is what its authoredmaxover adatefield means, while the preview VALUE beside it stays wrong until the evaluator is fixed. The descriptor is not withheld to accommodate a producer defect — that would be a consumer-side fallback in descriptor form (Prime Directive #12), and it would make the response's account of a column depend on a bug. Before this change the preview was wrong twice (no descriptor AND a zeroed value); now it is wrong once, visibly.Neither is asserted in the new test, so fixing them will not have to red this file.
403for this session (GitHub access is not enabled for this session) andghis absent, so the fallback is one targeted MCPsearch_issuesfor de-duplication — which answeredAPI rate limit already exceeded. Filing without that de-duplication read is the one thing worse than not filing, so both findings are handed to the PM in the structured report with this reason attached, for filing againstpreview-evaluator.ts. They are recorded here so they cannot be lost with this session.Verification
All exit codes captured immediately after a single redirected command, never through a pipe.
pnpm --filter '@objectstack/service-analytics^...' buildpnpm --filter @objectstack/service-analytics testpnpm --filter @objectstack/service-analytics typecheck@objectstack/rest/@objectstack/runtime/@objectstack/clientpnpm build(full workspace)pnpm lint(repo-wideeslint . --no-inline-config, no narrowing)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsThe gate family was derived mechanically, never hand-built, and re-derived on the final head. Two of the 56 first answered
3 = PREREQUISITE NOT METand were converted into real measurements rather than reported as passes:check:dual-build-cjs-loadsneeded the full workspace build, andcheck:type-check-debtOOMed under--max-old-space-size=4096(the gate names the caller'sNODE_OPTIONSas the constraint) and was re-run at 8192, where its re-measure leg reports140 raw tsc error(s) total, none above its recorded number.Reverse verification, direction predicted before running. Deleting the
enrichResultColumns(previewResult, …)call from the preview branch must red every preview assertion and leave the dimension-value-label fence case green — ordinary direction, no inversion, since the change only ADDS descriptor keys. Committed first, then mutated; mutation proven on disk by agit hash-objectdelta (e7a40f97…to667e7c59…) plus anchored counts (1 to 0, with the live-path call still present at 1); restored undertrap … EXIT INT TERMon an absolute path; restore proven by blob equality toHEAD:and an emptygit diff HEAD. Measured 11 red / 1 green, and the 1 green is exactly the fence case.Changeset
patchon@objectstack/service-analytics. It changes what a published wire surface produces on one path —POST /analytics/dataset/querywithpreview=draftnow returns column descriptors it previously omitted. Additive on the response: no key changes meaning, none is removed, and the live path is byte-identical.Not addressed here
min/maxover a text / select / lookup field is still described astype: "number"in the analytics response #16098 —min/maxover text/select/lookup still typednumber. Different population, its own card;measure-result-type.tsis untouched by this PR.$icontainsstill compilestranslate()on theunknowndialect arm, so a SQLite datasource whose dialect is unanswered still fails to parse — PR #16020's measured residue #16028 —text-match-sql.tsis untouched.Every reading above — the union of gates, lint, the package suite, the consumers and the full build — was taken on final head
4636bcb2b, which is what this branch points at. No prediction is made here about CI state.🤖 Generated with Claude Code
https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
Generated by Claude Code